<?php
/**
 * @file			ziyouApi_sdk.class.php
 * @CopyRight		(C)1996-2099 SINA Inc.
 * @Project			Ziyou
 * @Author			liujz <jianzhou@staff.sina.com.cn>
 * @Create Date:	2012-01-13
 * @Brief			PHP SDK for ziyou.com
 */
//配置文件
include_once( 'config.php' );
//HTTP客户端实现，如果你的环境没有CURL你可以使用 FSOPEN实现之
include_once( 'curl_http.class.php' );

class ziyouApi_sdk {
	var $_auth 				= 'smpauth';			// 认证方式
	var $_timeKey 			= 'vy_time';			// 时间戳KEY
	var $_signJey			= 'vy_sign';			// 请求参数签名KEY
	var $_sesKey			= '_ziyou_token';		// 保存用户token的Session KEY
	var $_cookieTokenKey 	= 'VYJ__TOKEN__KEY';	// 传递用户token的cookie KEY
	var $userHander			= array();
	
	var $zyAppkey			= '';					//自游API，APPKEY
	var $zyAppsec			= '';					//自游API，APP SECRETKEY
	
	var $http				= null;
	
	/**
	* SDK初始化
	* 
	*/
	function ziyouApi_sdk(){
		$this->http = new curl_http();
		$this->userHander = array(
			//将获取到的用户状态通过这个回调保存，开发者可以自定义保存以及获取的回调，默认保存在SESSION中
			'set'=>array($this, '_setToken'),
			//将获取到的用户状态通过这个回调获取，开发者可以自定义保存以及获取的回调，默认保存在SESSION中
			'get'=>array($this, '_getToken')
		);
		
		$this->zyAppkey = ZIYOU_SOURCE;
		$this->zyAppsec = ZIYOU_SKEY;
		$this->setOption();
	}
	
	/**
	* 设置SDK所使用的 APPKEY 和 SECRETKEY 
	* 默认使用的是，配置文件中的 ZIYOU_SOURCE 与 ZIYOU_SKEY 常量
	* 
	* @param mixed $appKey
	* @param mixed $secKey
	*/
	function setApp($appKey, $secKey){
	  	$this->zyAppkey = $appKey;	
		$this->zyAppsec = $secKey;
		return true;
	}
	/**
	* 获取当前用户的TOKEN
	*/
	function getToken(){
		return $this->_userStatHander('get');
	}
	/**
	* 保存（设置）一个TOKEN
	* 
	* @param mixed $tk
	*/
	function setToken($tk){
		return $this->_userStatHander('set', $tk);
	}
	
	/**
	* 退出当前用户,内部实现为 通过 hander 清除已存储的 登录凭证TOKEN
	*/
	function logout(){
		return $this->_userStatHander('set', '');
	}

	/**
	* 内置的用于保存用户登录凭证 TOKEN 的方法,内置方法使用SESSION保存
	* 
	* @param mixed $token	待保存的用户的登录凭证
	* @return				true	
	*/
	function _setToken($token){
		$_SESSION[$this->_sesKey] = $token;
		return true;
	}
	
	/**
	 * 获取用户TOKEN
	 */

	/**
	* 内置的用于获取用户登录凭证 TOKEN 的方法,内置方法使用SESSION保存
	* @return	返回一个登录凭证
	*/
	function _getToken(){
		$token = isset($_SESSION[$this->_sesKey]) ? $_SESSION[$this->_sesKey] : '';
		return $token;
	}

	/**
	* 调用API，请求参数必须签名
	* 
	* @param string $params		所有参数构成的关联数组
	* @return string			返回一个签名字符串，用于较检
	*/
	function signRequest($params){
		$params[$this->_timeKey] = time();
		ksort($params);
		
		$req 	 = urldecode(http_build_query($params));
		$secret	 = $this->zyAppsec;
		
		$params[$this->_signJey] = md5($req."&$secret");
		return $params;
	}

	/**
	* 调用自游网API
	* @param $apiRoute		api的路由(即文件名),如：/youji/get 表示获取游记信息
	* @param $params		传递参数 为关联数组， 如：array('youji_id'=>'6803,259937386656')	
	* @param $method		http方法[get/post/delete]
	* @return				返回值为 API 返回值，或者 错误信息
	* 
	* 通用的错误信息，结构格式实例：
	* Array (
	*	[api] => /account/verify.smpauth
	*	[errno] => 210002
	*	[err] => 授权检查失败，当前接口必须登录用户才能访问!
	* )；
	* 
	*/
	function api($apiRoute, $params=array(), $method='get'){
		$apiRoute = ltrim($apiRoute, "/");
		$this->lastErr(false);
		$oHttp 	= $this->getHttpObject();
		$token	= $this->getToken();
		if ($token) {
			$this->http->setCookie($this->_cookieTokenKey, $token);
		} else {
			$params['source'] = $this->zyAppkey;
		}
		
		$url 	= substr($apiRoute, 4, 3)=='://' ? $apiRoute : $this->_apiHost.$apiRoute.'.'.$this->_auth;
		$p		= $this->signRequest($params);
		
		$result	= $this->http->call($url, $p, $method);
		$result	= json_decode($result,1);

		//$result = json_decode(preg_replace('#(?<=[,\{\[])\s*("\w+"):(\d{6,})(?=\s*[,\]\}])#si', '${1}:"${2}"', $result) , TRUE);
		if (isset($result['errno'])){
			$this->lastErr($result);
		}
		return $result;
	}
	
	function setOption($p=array()){
		$this->_apiHost = isset($p['host']) ? rtrim($p['host'],'/').'/' : ZIYOU_API_URL;
		$this->_auth = isset($p['auth']) ?  $p['auth'] : 'smpauth';
	}
	
	/**
	* 获取最后一次错误，如果无错误，则返回 FALSE
	* 
	* @param mixed $err		有参数时表示，存储错误信息，外部调用时，不需要参数
	* @return				返回最后一次错误信息，如果无错误返回 FALSE
	*/
	function lastErr($err=false){
		static $e=false;
		if (func_num_args()){ $e = $err; }
		return $e;
	}
	

	//设置用户状态处理器,包含用户状态的 存储，获取，清除
	/**
	* 设置一组（两个）用于保存用户登录状态（登录凭证TOKEN）的 hander Callback 调用
	* 
	* @param mixed $hander 用户状态的 hander 结构如下 ,注：如果get,set 指定的是字符串则当成函数处理
	* $hander = array(
	*	'get'=>array($hander,'utGet'),
	*	'set'=>array($hander,'utSet')
	* );
	* 
	* 上面的 $hander 类实例， 必须包含如下方法
	* hander::utGet()			SDK 会通过这个方法，获取 用户登录凭证 TOKEN
	* hander::utSet($token)		SDK 会通过这个方法，保存 用户登录凭证 TOKEN
	*/
	function setUserHander($hander){
		$this->userHander = $hander;
	}

	/**
	* 调用用户状态的 hander，调用或者获取 TOKEN
	* 
	* @param mixed $micFunc		操作类型方法，可选值为 set,get
	* @param mixed $p			传递的参数（TOKEN）当操作类型为 set 时有效
	*/
	function _userStatHander($micFunc, $p=false){
		$micFunc = $this->userHander[$micFunc];
		if (is_string($micFunc)){
			return $micFunc($p);
		}else{
			return $micFunc[0]->$micFunc[1]($p); 
		}
	}

	/**
	* 用户登录，登录后，SDK会自动根据开发者设置的 用户状态存储hander 保存登录状态
	* 以便持续的访问需要登录的API接口，详情见 
	* 
	* @param mixed $user	用户名
	* @param mixed $pwd		密码
	*/
	function login($user, $pwd){
		$param['email']  	= $user;
		$param['password'] 	= $pwd;
		
		$rst = $this->api('/account/login', $param, 'post');
		//var_dump($rst);
		if ( isset($rst['token']) ) {
			return $this->setToken($rst['token']);
		} else {
			return false;
		}
	}

	/**
	* 获取http请求对象，默认是curl,如果用户环境中没有　curl 扩展
	* @param $oHttp
	*/
	function getHttpObject($oHttp=''){
		return $this->http;
	}
	
	/**
	* 获取第三方认证的登录URL，开发者需要生成不同的第三文登录链接
	* 
	* @param mixed $callback	一个登录回调的URL，在第三方页面中登录成功以后，会跳转到此URL
	* @param mixed $type		第三文账号的类型,目前支持：[sina/qq/renren]
	*/
	function getAuthorizeURL($callback, $type='sina'){
    	$req  = array('type'=>$type, 'source'=>$this->zyAppkey, 'callback'=>urlencode($callback));
    	return $this->_apiHost."account/connect.nsauth?".http_build_query( $this->signRequest($req) );
	}
	
	/**
	* 使用第三方登录时，开发者要在回调页面程序中需调用这个方法，用于 获取、解释、保存用户登录凭证（TOKEN）
	* @return	返回登录凭证 TOKEN
	*/
	function getAuthorizeToken(){
		$token 	= '';
		$tk 	= isset($_GET['_tk']) ? $_GET['_tk'] : '';
		if ($tk) { $token = $this->decryptUrl($tk); }
		$this->setToken($token);
		return $token;
	}

	/**
	* 解析Token
	* 
	* @param string $token 		密文TOKEN
	* @return string			解释后的明文TOKEN
	*/
	function decryptUrl($token){
		// 解析Token
		if ( $token ){
			$info	= parse_url(ZIYOU_CALLBACK_URL);
			$key	= $this->zyAppkey.'#'.$info['host'];
			$token	= self::authcode($token, 'DECODE', $key);
			return $token;
		}
		return '';
	}
	
	/**
	* 工具函数，加密或者解释，一段文本
	* 
	* @param mixed $string		待加密或者解释的文本
	* @param mixed $operation	操作，可先 DECODE|ENCODE 分别表示解密或者加密
	* @param string $key		加密或者解释使用的公钥
	* @param mixed $expiry		密文过期时间，时间戳
	* @return string			返回密文或者，明文
	*/
	function authcode($string, $operation='DECODE', $key='', $expiry=0) {   
	    // 动态密匙长度，相同的明文会生成不同密文就是依靠动态密匙   
	    $ckey_length = 4;   
	       
	    // 密匙   
	    $key = md5($key ? $key : VYJ_AUTH_CODE);   
	       
	    // 密匙a会参与加解密   
	    $keya = md5(substr($key, 0, 16));   
	    // 密匙b会用来做数据完整性验证   
	    $keyb = md5(substr($key, 16, 16));   
	    // 密匙c用于变化生成的密文   
	    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';   
	    // 参与运算的密匙   
	    $cryptkey = $keya.md5($keya.$keyc);   
	    $key_length = strlen($cryptkey);   
	    // 明文，前10位用来保存时间戳，解密时验证数据有效性，10到26位用来保存$keyb(密匙b)，解密时会通过这个密匙验证数据完整性   
	    // 如果是解码的话，会从第$ckey_length位开始，因为密文前$ckey_length位保存 动态密匙，以保证解密正确   
	    $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;   
	    $string_length = strlen($string);   
	    $result = '';   
	    $box = range(0, 255);   
	    $rndkey = array();   
	    // 产生密匙簿   
	    for($i = 0; $i <= 255; $i++) {   
	        $rndkey[$i] = ord($cryptkey[$i % $key_length]);   
	    }   
	    // 用固定的算法，打乱密匙簿，增加随机性，好像很复杂，实际上对并不会增加密文的强度   
	    for($j = $i = 0; $i < 256; $i++) {   
	        $j = ($j + $box[$i] + $rndkey[$i]) % 256;   
	        $tmp = $box[$i];   
	        $box[$i] = $box[$j];   
	        $box[$j] = $tmp;   
	    }   
	    // 核心加解密部分   
	    for($a = $j = $i = 0; $i < $string_length; $i++) {   
	        $a = ($a + 1) % 256;   
	        $j = ($j + $box[$a]) % 256;   
	        $tmp = $box[$a];   
	        $box[$a] = $box[$j];   
	        $box[$j] = $tmp;   
	        // 从密匙簿得出密匙进行异或，再转成字符   
	        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));   
	    }   
	    if($operation == 'DECODE') {   
	        // substr($result, 0, 10) == 0 验证数据有效性   
	        // substr($result, 0, 10) - time() > 0 验证数据有效性   
	        // substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16) 验证数据完整性   
	        // 验证数据有效性，请看未加密明文的格式   
	        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {   
	            return substr($result, 26);   
	        } else {   
	            return '';   
	        }   
	    } else {   
	        // 把动态密匙保存在密文里，这也是为什么同样的明文，生产不同密文后能解密的原因   
	        // 因为加密后的密文可能是一些特殊字符，复制过程可能会丢失，所以用base64编码   
	        return $keyc.str_replace('=', '', base64_encode($result));   
	    }   
	}
}
